From 31b825b183bfae702c8ceb2fa48b29a4b830cf73 Mon Sep 17 00:00:00 2001
From: Fuwn <contact@fuwn.net>
Date: Tue, 2 Jun 2026 00:25:29 +0000
Subject: fix(security): sanitize badge_wall_css server-side, render via
 textContent

Custom badge-wall CSS was sanitised only client-side with a fragile regex
and injected via innerHTML, while the stored value stayed raw. Sanitise
at the write boundary instead (setCSS, covering both the REST and GraphQL
paths) with a css-tree pass that parses leniently and drops @import,
behavior/-moz-binding, expression()/javascript: values, and </style>
break-out attempts; render with textContent instead of innerHTML so
break-out is impossible by construction (CSP already blocks inline
script). css-tree stays server-only. A behaviour-gate test confirms
ordinary CSS (backdrop-filter, content, url(), @media, @keyframes) is
preserved while the dangerous constructs are removed.

The previous regex also silently stripped all `content:` declarations;
those now render correctly.
---
 src/routes/user/[user]/badges/+page.svelte | 13 +------------
 1 file changed, 1 insertion(+), 12 deletions(-)

(limited to 'src/routes/user/[user]/badges/+page.svelte')

diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte
index dc43fc08..605f5675 100644
--- a/src/routes/user/[user]/badges/+page.svelte
+++ b/src/routes/user/[user]/badges/+page.svelte
@@ -40,21 +40,10 @@ $: preferences = $BadgeWallUser.fetching
 	: ($BadgeWallUser.data?.User?.preferences as Preferences | undefined);
 
 $: if (browser && preferences && preferences.badge_wall_css) {
-	const sanitise = (css: string) =>
-		css
-			.replace(/\/\*[\s\S]*?\*\//g, "")
-			.replace(/<\/?[^>]+(>|$)/g, "")
-			.replace(
-				/(expression|javascript|vbscript|onerror|onload|onclick|onmouseover|onmouseout|onmouseup|onmousedown|onkeydown|onkeyup|onkeypress|onblur|onfocus|onsubmit|onreset|onselect|onchange|ondblclick):/gi,
-				"",
-			)
-			.replace(/(behaviour|behavior|moz-binding|content):/gi, "")
-			.replace(/\s+/g, " ")
-			.trim();
 	const style = document.createElement("style");
 
 	style.dataset.badgeWall = "true";
-	style.innerHTML = sanitise(preferences.badge_wall_css);
+	style.textContent = preferences.badge_wall_css;
 
 	document.head.appendChild(style);
 }
-- 
cgit v1.2.3

